Maîtrisez l'API User Timing pour créer des métriques de performance personnalisées et significatives. Allez au-delà des signaux web essentiels pour identifier les goulots d'étranglement et optimiser l'expérience utilisateur.
Maîtriser la performance frontend : Une analyse approfondie de l'API User Timing
Dans le paysage numérique moderne, la performance frontend n'est pas un luxe ; c'est une exigence fondamentale pour réussir. Pour un public mondial, un site web lent et peu réactif peut entraîner la frustration des utilisateurs, une diminution de l'engagement et un impact négatif direct sur les résultats commerciaux. Nous disposons d'excellentes métriques standardisées comme les Signaux Web Essentiels (Core Web Vitals) (Largest Contentful Paint, First Input Delay, Cumulative Layout Shift) qui nous donnent une compréhension de base de l'expérience utilisateur. Cependant, ces métriques, bien que cruciales, ne racontent qu'une partie de l'histoire.
Qu'en est-il de la performance des fonctionnalités spécifiques à une application ? Combien de temps faut-il pour que les résultats de recherche apparaissent après qu'un utilisateur a tapé une requête ? Combien de temps votre composant complexe de visualisation de données met-il à s'afficher après avoir reçu des données d'une API ? Comment une nouvelle fonctionnalité impacte-t-elle la vitesse des transitions de route de votre application monopage (SPA) ? Les métriques standard ne peuvent pas répondre à ces questions granulaires et critiques pour l'entreprise. C'est là que l'API User Timing entre en jeu, donnant aux développeurs le pouvoir de créer des mesures de performance personnalisées et de haute précision, adaptées à leurs applications uniques.
Ce guide complet vous expliquera tout ce que vous devez savoir pour exploiter l'API User Timing, des concepts de base des marques et des mesures aux techniques avancées utilisant le PerformanceObserver. À la fin, vous serez équipé pour aller au-delà des métriques génériques et commencer à raconter l'histoire de performance unique de votre application.
Qu'est-ce que l'API Performance ? Un contexte plus large
Avant de nous plonger dans le User Timing, il est important de comprendre qu'il fait partie d'une suite plus large d'outils collectivement connus sous le nom d'API Performance. Cette API de navigateur donne accès à des données de synchronisation de haute précision liées à la navigation, au chargement des ressources, et plus encore. L'objet global `window.performance` est votre point d'entrée vers cette puissante boîte à outils.
L'API Performance est composée de plusieurs interfaces, notamment :
- Timing de navigation (Navigation Timing) : Fournit des informations de synchronisation détaillées sur le processus de navigation du document, telles que le temps passé sur les recherches DNS, les poignées de main TCP et la réception du premier octet.
- Timing des ressources (Resource Timing) : Offre des données de synchronisation réseau détaillées pour chaque ressource chargée par la page, y compris les images, les scripts et les fichiers CSS.
- Timing de rendu (Paint Timing) : Expose les temps pour le First Paint et le First Contentful Paint.
- Timing utilisateur (User Timing) : Le sujet de notre article, qui permet aux développeurs de créer leurs propres horodatages personnalisés (marques) et de mesurer la durée entre eux (mesures).
Ces API fonctionnent ensemble pour fournir une vue d'ensemble des performances de votre application. Notre objectif aujourd'hui est de maîtriser la partie User Timing, qui nous donne le pouvoir d'ajouter nos propres points de contrôle personnalisés à cette chronologie des performances.
Les concepts de base : Marques et Mesures
L'API User Timing est d'une simplicité trompeuse, tournant autour de deux concepts fondamentaux : les marques et les mesures. Pensez-y comme à l'utilisation d'un chronomètre. Vous appuyez sur un bouton pour marquer un temps de départ, et vous appuyez à nouveau pour marquer un temps de fin. La durée entre ces deux pressions est votre mesure.
Créer des marques de performance : `performance.mark()`
Une 'marque' est un horodatage nommé de haute précision enregistré à un point spécifique de l'exécution de votre application. C'est comme planter un drapeau sur votre chronologie des performances. Vous pouvez créer autant de marques que nécessaire pour identifier les moments clés d'un parcours utilisateur ou du cycle de vie d'un composant.
La syntaxe est simple :
performance.mark(markName, [markOptions]);
markName: Une chaîne de caractères représentant le nom unique de votre marque. Choisissez des noms descriptifs !markOptions(optionnel) : Un objet qui peut contenir une propriétédetailpour attacher des métadonnées supplémentaires, et unstartTimepour spécifier un horodatage personnalisé.
Exemple de base : Marquer un événement
Disons que nous voulons marquer le début d'un appel de fonction important.
function processLargeDataset() {
// Placer un marqueur juste avant le début du travail lourd
performance.mark('processLargeDataset:start');
// ... logique de calcul lourde ...
console.log('Traitement du jeu de données terminé.');
// Placer un autre marqueur une fois terminé
performance.mark('processLargeDataset:end');
}
processLargeDataset();
Dans cet exemple, nous avons créé deux horodatages dans la chronologie des performances du navigateur : `processLargeDataset:start` et `processLargeDataset:end`. Pour l'instant, ce ne sont que des points dans le temps. Leur véritable puissance se révèle lorsque nous les utilisons pour créer une mesure.
Ajouter du contexte avec la propriété `detail`
Parfois, un horodatage seul ne suffit pas. Vous pourriez vouloir inclure un contexte supplémentaire sur ce qui se passait à ce moment-là . La propriété `detail` est parfaite pour cela. Elle peut contenir toutes les données qui peuvent être clonées structurellement (comme des objets, des tableaux, des chaînes de caractères, des nombres).
Imaginons que nous marquons le début du rendu d'un composant et que nous voulons savoir combien d'éléments il affichait.
function renderProductList(products) {
const itemCount = products.length;
performance.mark('ProductList:render:start', {
detail: {
itemCount: itemCount,
source: 'initial-load'
}
});
// ... logique de rendu du composant ...
performance.mark('ProductList:render:end');
}
const sampleProducts = new Array(1000).fill(0);
renderProductList(sampleProducts);
Ce contexte additionnel est inestimable lors de l'analyse ultérieure des données de performance. Vous pourriez, par exemple, corréler les temps de rendu avec le nombre d'éléments pour voir s'il existe une relation linéaire ou exponentielle.
Créer des mesures de performance : `performance.measure()`
Une 'mesure' capture la durée entre deux points dans le temps. C'est le calcul qui vous dit "combien de temps" quelque chose a pris. Le plus souvent, vous mesurerez le temps entre deux de vos marques personnalisées.
La syntaxe a quelques variations :
performance.measure(measureName, startMarkOrOptions, [endMark]);
measureName: Une chaîne de caractères représentant le nom unique de votre mesure.startMarkOrOptions(optionnel) : Une chaîne de caractères avec le nom de la marque de début. Peut aussi être un objet d'options avec `start`, `end`, `duration` et `detail`.endMark(optionnel) : Une chaîne de caractères avec le nom de la marque de fin.
Exemple de base : Mesurer la durée d'une fonction
Reprenons notre exemple `processLargeDataset` et mesurons réellement combien de temps il a fallu.
function processLargeDataset() {
performance.mark('processLargeDataset:start');
// ... logique de calcul lourde ...
performance.mark('processLargeDataset:end');
// Maintenant, créons la mesure
performance.measure(
'processLargeDataset:duration',
'processLargeDataset:start',
'processLargeDataset:end'
);
}
processLargeDataset();
Après l'exécution de ce code, le tampon de performance du navigateur contiendra une nouvelle entrée nommée `processLargeDataset:duration`. Cette entrée aura une propriété `duration` contenant le temps précis, en millisecondes, qui s'est écoulé entre les marques de début et de fin.
Scénarios de mesure avancés
La méthode `measure()` est très flexible. Vous n'avez pas toujours besoin de fournir deux marques.
- Du début de la navigation à une marque : Vous pouvez mesurer le temps écoulé depuis le début de la navigation de la page jusqu'à l'une de vos marques personnalisées. C'est incroyablement utile pour mesurer des choses comme le "Temps jusqu'au composant interactif".
// Mesurer depuis le début de la navigation jusqu'à ce que le composant principal soit prêt performance.measure('timeToInteractiveHeader', 'navigationStart', 'headerComponent:ready'); - D'une marque jusqu'à maintenant : Si vous omettez `endMark`, la mesure sera calculée de votre `startMark` à l'instant présent.
// Mesurer depuis la marque de début jusqu'à l'exécution de cette ligne de code performance.measure('timeSinceDataRequest', 'api:fetch:start'); - Utiliser l'objet d'options : Vous pouvez également passer un objet de configuration pour définir la mesure, ce qui est utile pour ajouter une propriété `detail`.
performance.measure('complexRender:duration', { start: 'complexRender:start', end: 'complexRender:end', detail: { renderType: 'canvas' } });
Accéder et effacer les entrées de performance
Créer des marques et des mesures n'est que la moitié du travail. Vous avez besoin d'un moyen de récupérer ces données pour les analyser. L'objet `performance` fournit plusieurs méthodes pour cela.
performance.getEntries(): Renvoie un tableau de toutes les entrées de performance dans le tampon (y compris les timings des ressources, de navigation, etc.).performance.getEntriesByType(type): Renvoie un tableau d'entrées d'un type spécifique. Vous utiliserez le plus souvent `performance.getEntriesByType('mark')` et `performance.getEntriesByType('measure')`.performance.getEntriesByName(name, [type]): Renvoie un tableau d'entrées avec un nom spécifique (et optionnellement, un type spécifique).
Exemple : Afficher les mesures dans la console
// Après avoir exécuté nos exemples précédents...
const allMeasures = performance.getEntriesByType('measure');
console.log(allMeasures);
// Un objet d'entrée de mesure ressemble à ceci :
// {
// "name": "processLargeDataset:duration",
// "entryType": "measure",
// "startTime": 12345.67,
// "duration": 150.89
// }
const specificMeasure = performance.getEntriesByName('processLargeDataset:duration');
console.log(`Le traitement a pris : ${specificMeasure[0].duration}ms`);
Important : Nettoyer le tampon de performance
Le tampon de performance du navigateur n'est pas infini. Pour éviter les fuites de mémoire et garder vos mesures pertinentes, il est recommandé de nettoyer les marques et les mesures que vous avez créées une fois que vous avez terminé avec elles.
performance.clearMarks([name]): Efface toutes les marques, ou seulement les marques avec le nom spécifié.performance.clearMeasures([name]): Efface toutes les mesures, ou seulement les mesures avec le nom spécifié.
Un modèle courant consiste à récupérer les données, les traiter ou les envoyer, puis les effacer.
function analyzeAndClear() {
const myMeasures = performance.getEntriesByName('processLargeDataset:duration');
// Envoyer myMeasures Ă un service d'analyse...
sendToAnalytics(myMeasures);
// Nettoyer pour libérer de la mémoire
performance.clearMarks('processLargeDataset:start');
performance.clearMarks('processLargeDataset:end');
performance.clearMeasures('processLargeDataset:duration');
}
Cas d'usage pratiques et concrets pour le User Timing
Maintenant que nous comprenons les mécanismes, explorons comment appliquer l'API User Timing pour résoudre des défis de performance du monde réel. Ces exemples sont indépendants du framework et peuvent être adaptés à n'importe quelle pile frontend.
1. Mesurer la durée des appels API
Comprendre combien de temps votre application attend les données est essentiel. Vous pouvez facilement envelopper votre logique de récupération de données avec des marques et des mesures.
async function fetchUserData(userId) {
const markStart = `api:getUser:${userId}:start`;
const markEnd = `api:getUser:${userId}:end`;
const measureName = `api:getUser:${userId}:duration`;
performance.mark(markStart);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('La réponse du réseau n\'était pas ok');
}
return await response.json();
} catch (error) {
console.error('Erreur de fetch :', error);
// Vous pouvez même ajouter des détails sur les erreurs !
performance.mark(markEnd, { detail: { status: 'error', message: error.message } });
} finally {
// S'assurer que la marque de fin et la mesure sont toujours créées
if (performance.getEntriesByName(markEnd).length === 0) {
performance.mark(markEnd, { detail: { status: 'success' } });
}
performance.measure(measureName, markStart, markEnd);
}
}
fetchUserData('123');
Ce modèle fournit des timings précis pour chaque appel API, vous permettant d'identifier les points de terminaison lents directement à partir des données des utilisateurs réels.
2. Suivre les temps de rendu des composants dans les SPA
Pour des frameworks comme React, Vue ou Angular, mesurer le temps nécessaire pour qu'un composant se monte et s'affiche est un cas d'usage principal. Cela aide à identifier les composants complexes qui pourraient ralentir votre application.
Exemple avec les Hooks React :
import React, { useLayoutEffect, useEffect, useRef } from 'react';
function MyHeavyComponent({ data }) {
const componentId = useRef(`MyHeavyComponent-${Math.random()}`).current;
const markStartName = `${componentId}:render:start`;
const markEndName = `${componentId}:render:end`;
const measureName = `${componentId}:render:duration`;
// useLayoutEffect s'exécute de manière synchrone après toutes les mutations du DOM.
// C'est l'endroit idéal pour marquer le début de la mesure de rendu.
useLayoutEffect(() => {
performance.mark(markStartName);
}, []); // S'exécute uniquement au montage initial
// useEffect s'exécute de manière asynchrone une fois le rendu appliqué à l'écran.
// C'est un bon endroit pour marquer la fin.
useEffect(() => {
performance.mark(markEndName);
performance.measure(measureName, markStartName, markEndName);
// Affiche le résultat pour la démonstration
const measure = performance.getEntriesByName(measureName)[0];
if (measure) {
console.log(`${measureName} a pris ${measure.duration}ms`);
}
// Nettoyage
performance.clearMarks(markStartName);
performance.clearMarks(markEndName);
performance.clearMeasures(measureName);
}, []); // S'exécute uniquement au montage initial
return (
// ... JSX pour le composant lourd ...
);
}
3. Quantifier les parcours utilisateur critiques
L'utilisation la plus percutante du User Timing est de mesurer les interactions utilisateur en plusieurs étapes qui sont critiques pour votre entreprise. Cela transcende les simples métriques techniques et mesure la vitesse perçue des fonctionnalités de base de votre application.
Considérez un processus de paiement e-commerce :
const checkoutButton = document.getElementById('checkout-btn');
checkoutButton.addEventListener('click', () => {
// 1. L'utilisateur clique sur le bouton 'valider la commande'
performance.mark('checkout:journey:start');
// ... code pour valider le panier, naviguer vers la page de paiement, etc. ...
});
// Sur la page de paiement, une fois le formulaire de paiement rendu et interactif
function onPaymentFormReady() {
performance.mark('checkout:paymentForm:ready');
performance.measure('checkout:timeToPaymentForm', 'checkout:journey:start', 'checkout:paymentForm:ready');
}
// Une fois le paiement traité avec succès et l'écran de confirmation affiché
function onPaymentSuccess() {
performance.mark('checkout:journey:end');
performance.measure('checkout:totalJourney:duration', 'checkout:journey:start', 'checkout:journey:end');
// Vous disposez maintenant de deux métriques puissantes à analyser et optimiser.
}
4. Tester les améliorations de performance avec l'A/B testing
Lorsque vous refactorisez un morceau de code ou introduisez un nouvel algorithme, comment prouvez-vous qu'il est réellement plus rapide pour les utilisateurs réels ? Le User Timing fournit des données objectives pour les tests A/B.
Imaginez que vous ayez deux algorithmes de tri différents que vous voulez tester :
function sortProducts(products, algorithmVersion) {
const markStart = `sort:v${algorithmVersion}:start`;
const markEnd = `sort:v${algorithmVersion}:end`;
const measureName = `sort:v${algorithmVersion}:duration`;
performance.mark(markStart);
if (algorithmVersion === 'A') {
// ... exécuter l'ancien algorithme de tri ...
} else {
// ... exécuter le nouvel algorithme de tri optimisé ...
}
performance.mark(markEnd);
performance.measure(measureName, markStart, markEnd);
}
// En fonction d'un indicateur de test A/B, vous appelleriez l'un ou l'autre.
// Plus tard, dans vos analyses, vous pourrez comparer la durée moyenne de
// 'sort:vA:duration' par rapport à 'sort:vB:duration' pour voir lequel était le plus rapide.
Visualiser et analyser vos métriques personnalisées
Créer des métriques personnalisées est inutile si vous n'analysez pas les données. Il y a deux manières principales d'aborder cela : localement pendant le développement et agrégées en production.
Utiliser les outils de développement du navigateur
Les navigateurs modernes comme Chrome et Firefox ont un excellent support pour visualiser les marques et mesures du User Timing dans leurs outils de profilage de performance.
- Ouvrez les outils de développement de votre navigateur (F12 ou Ctrl+Shift+I).
- Allez dans l'onglet Performance.
- Commencez à enregistrer un profil, puis effectuez les actions dans votre application qui déclenchent vos marques et mesures personnalisées.
- ArrĂŞtez l'enregistrement.
Dans la vue de la chronologie, vous trouverez une ligne dédiée appelée Timings. Vos marques personnalisées apparaîtront comme des lignes verticales, et vos mesures seront affichées comme des barres colorées montrant leur durée. Survoler ces éléments révélera leurs noms et leurs timings exacts. C'est un moyen incroyablement puissant de déboguer les problèmes de performance pendant le développement.
Envoyer les données aux services d'analyse et de RUM
Pour la surveillance en production, vous devez collecter ces données auprès de vos utilisateurs et les envoyer à un emplacement central pour l'agrégation et l'analyse. C'est une partie essentielle de la Surveillance de l'Utilisateur Réel (RUM).
Le flux de travail général est le suivant :
- Collecter les mesures de performance qui vous intéressent.
- Les formater dans une charge utile appropriée (par exemple, JSON).
- Envoyer la charge utile à un point de terminaison d'analyse. Il peut s'agir d'un service tiers comme Datadog, New Relic, Sentry, ou même Google Analytics (via des événements personnalisés), ou d'un backend personnalisé que vous contrôlez.
function sendPerformanceData() {
// Nous ne nous intéressons qu'à nos mesures d'application personnalisées
const appMeasures = performance.getEntriesByType('measure').filter(
(entry) => entry.name.startsWith('app:') // Utilisez une convention de nommage !
);
if (appMeasures.length > 0) {
const payload = JSON.stringify(appMeasures.map(measure => ({
name: measure.name,
duration: measure.duration,
startTime: measure.startTime,
details: measure.detail, // Envoyer notre contexte riche
path: window.location.pathname // Ajouter plus de contexte
})));
// Utiliser navigator.sendBeacon pour un envoi de données fiable et non bloquant
navigator.sendBeacon('https://analytics.example.com/performance', payload);
// Nettoyer les mesures qui ont été envoyées
appMeasures.forEach(measure => {
performance.clearMeasures(measure.name);
// Effacer également les marques associées
});
}
}
// Appeler cette fonction à un moment approprié, par ex., lorsque la page est sur le point d'être quittée
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
sendPerformanceData();
}
});
Techniques avancées et meilleures pratiques
Pour vraiment maîtriser l'API User Timing, examinons quelques fonctionnalités avancées et meilleures pratiques qui rendront votre instrumentation plus robuste et efficace.
Utiliser `PerformanceObserver` pour une surveillance asynchrone
Les méthodes `getEntries*()` vous obligent à interroger manuellement le tampon de performance. Cela présente deux inconvénients : vous pourriez exécuter votre vérification trop tard et manquer des entrées si le tampon s'est rempli et a été effacé, et l'interrogation elle-même peut avoir un coût de performance mineur. La solution moderne et préférée est le `PerformanceObserver`.
Un `PerformanceObserver` vous permet de vous abonner aux événements d'entrée de performance. Votre fonction de rappel sera invoquée de manière asynchrone chaque fois que de nouvelles entrées des types que vous observez sont enregistrées.
// 1. Créer une fonction de rappel pour gérer les nouvelles entrées
const observerCallback = (list) => {
for (const entry of list.getEntries()) {
console.log('Nouvelle mesure observée :', entry.name, entry.duration);
// Ici, vous pouvez immédiatement envoyer l'entrée à votre service d'analyse
// sans avoir besoin d'interroger ou d'attendre.
}
};
// 2. Créer l'instance de l'observateur
const observer = new PerformanceObserver(observerCallback);
// 3. Commencer à observer les types d'entrée 'mark' et 'measure'
// L'option 'buffered: true' garantit que vous recevez les entrées qui ont été créées
// *avant* l'enregistrement de l'observateur.
observer.observe({ entryTypes: ['mark', 'measure'], buffered: true });
// Désormais, chaque fois que performance.mark() ou performance.measure() est appelé n'importe où
// dans votre application, le rappel observerCallback sera déclenché avec la nouvelle entrée.
// PourarrĂŞter l'observation plus tard :
// observer.disconnect();
L'utilisation de `PerformanceObserver` est plus efficace, plus fiable, et devrait être votre choix par défaut pour collecter des données de performance dans un environnement de production.
Établir une convention de nommage claire
À mesure que votre application grandit, vous accumulerez de nombreuses métriques personnalisées. Sans une convention de nommage cohérente, vos données deviendront difficiles à filtrer et à analyser. Adoptez un modèle qui fournit du contexte.
Une bonne convention pourrait ĂŞtre : [nomApp]:[fonctionnaliteOuComposant]:[evenement]:[statut]
ecom:ProductGallery:render:startecom:ProductGallery:render:endecom:ProductGallery:render:durationadmin:DataTable:fetchApi:startadmin:DataTable:fetchApi:duration
Cette structure rend trivial le filtrage de toutes les métriques liées à la `ProductGallery` ou la recherche de toutes les durées `fetchApi` dans l'ensemble de l'application.
Abstraire dans un service utilitaire
Pour assurer la cohérence et réduire le code répétitif, encapsulez les appels à `performance` dans votre propre module ou service utilitaire. Cela facilite également l'activation ou la désactivation de la surveillance des performances en fonction de l'environnement.
// service-performance.js
const IS_PERFORMANCE_MONITORING_ENABLED = process.env.NODE_ENV === 'production' || window.location.search.includes('perf=true');
export const perfMark = (name, options) => {
if (!IS_PERFORMANCE_MONITORING_ENABLED) return;
performance.mark(name, options);
};
export const perfMeasure = (name, start, end) => {
if (!IS_PERFORMANCE_MONITORING_ENABLED) return;
performance.measure(name, start, end);
};
export const startJourney = (name) => {
perfMark(`${name}:start`);
};
export const endJourney = (name) => {
const startMark = `${name}:start`;
const endMark = `${name}:end`;
const measureName = `${name}:duration`;
perfMark(endMark);
perfMeasure(measureName, startMark, endMark);
// Optionnellement, effacer les marques ici
};
// Dans votre composant :
// import { startJourney, endJourney } from './service-performance';
// startJourney('ecom:checkout');
// ...plus tard...
// endJourney('ecom:checkout');
Conclusion : Reprendre le contrĂ´le de l'histoire des performances de votre application
Bien que les métriques standard comme les Signaux Web Essentiels fournissent un bilan de santé essentiel pour votre site web, elles n'éclairent pas la performance des fonctionnalités et des interactions qui rendent votre application unique. L'API User Timing est le pont qui comble cette lacune. Elle fournit un mécanisme simple mais profondément puissant pour mesurer ce qui compte vraiment pour vos utilisateurs et votre entreprise.
En mettant en œuvre des marques et des mesures personnalisées, vous transformez l'optimisation des performances d'un jeu de devinettes en une science basée sur les données. Vous pouvez identifier les fonctions, composants ou flux d'utilisateurs exacts qui causent des goulots d'étranglement, valider l'impact de vos efforts de refactorisation avec des chiffres objectifs, et finalement construire une expérience plus rapide et plus agréable pour votre public mondial.
Commencez petit. Identifiez le parcours utilisateur le plus critique de votre application, que ce soit la recherche d'un produit, la soumission d'un formulaire ou le chargement d'un tableau de bord de données. Instrumentez-le avec `performance.mark()` et `performance.measure()`. Analysez les résultats dans vos outils de développement. Une fois que vous verrez la clarté qu'il apporte, vous serez en mesure de raconter l'histoire complète des performances de votre application, une métrique personnalisée à la fois.